Skip to content

policy_fn: drop path strings, keep argv via sibling-thread freeze (#27)#29

Merged
congwang-mk merged 3 commits intomainfrom
policy-fn-drop-exec-strings
May 1, 2026
Merged

policy_fn: drop path strings, keep argv via sibling-thread freeze (#27)#29
congwang-mk merged 3 commits intomainfrom
policy-fn-drop-exec-strings

Conversation

@congwang-mk
Copy link
Copy Markdown
Contributor

Summary

Closes the dynamic-policy half of #27policy_fn decisions on user-memory strings were racy because the kernel re-reads after the supervisor's Continue. This PR makes the supported subset of policy_fn safe by construction.

Two changes to the event surface:

  1. Drop event.path and path_contains() from SyscallEvent, the FFI struct, and the Python dataclass. Path-based access control belongs in static Landlock rules (fs_read / fs_write / fs_deny), which are kernel-enforced and TOCTOU-immune. ctx.deny_path() stays for runtime additions.
  2. Keep event.argv, but make it TOCTOU-safe. Before the supervisor sends Continue for an execve/execveat, it PTRACE_SEIZE + PTRACE_INTERRUPTs every sibling thread of the calling tid (new sibling_freeze module). The kernel's post-Continue re-read happens with no other writer running, binding the kernel's view of argv to exactly what the supervisor inspected.

The freeze has effectively zero observable cost: execve's de_thread step kills sibling threads anyway. Single-threaded callers (typical case) freeze nothing. On ptrace failure (e.g. YAMA blocking), the supervisor falls back to the prior Landlock-bounded behavior rather than refusing the syscall.

Why ptrace is the right answer here

Considered alternatives: BPF CLONE_THREAD deny (rejected — breaks multi-threaded users), thread-count gate (Threads==1 ⇒ safe ⇒ expose argv, else None — rejected as a leaky API), pure drop of argv (loses the feature). ptrace is the only option that keeps a clean argv: Option<Vec<String>> always-populated-for-execve API. sandlock already uses ptrace in checkpoint.rs, so this isn't a new dependency; ~120 LOC in a self-contained module.

Test plan

  • cargo test --workspace — 182 tests pass, including restored test_policy_fn_execve_argv and test_policy_fn_deny_by_argv
  • python3 -m pytest python/tests/test_policy_fn.py — 17 tests pass
  • 2 new sibling_freeze unit tests
  • CI on Linux x86_64 + arm64

Files

  • crates/sandlock-core/src/sibling_freeze.rs (new) — freeze helper
  • crates/sandlock-core/src/policy_fn.rs — drop path, drop path_contains, keep argv/argv_contains
  • crates/sandlock-core/src/seccomp/notif.rs — stop reading path; freeze siblings before Continue on execve
  • crates/sandlock-ffi/src/lib.rs — drop path field from sandlock_event_t
  • python/src/sandlock/_sdk.py — mirror dataclass + ctypes changes
  • README — rewrite the policy_fn section, document the TOCTOU note explicitly

🤖 Generated with Claude Code

Signed-off-by: Cong Wang <cwang@multikernel.io>
Signed-off-by: Cong Wang <cwang@multikernel.io>
@congwang-mk congwang-mk force-pushed the policy-fn-drop-exec-strings branch from d8bb3f2 to 0b412ed Compare May 1, 2026 04:10
Signed-off-by: Cong Wang <cwang@multikernel.io>
@congwang-mk congwang-mk merged commit 222b9bc into main May 1, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant